﻿using PoliticalClassifier.Support;
using System.Drawing;

namespace PoliticalClassifier.PlotGenerator
{
    /// <summary>
    /// PlotHelper provides an API for create a plot image of the voter data.
    /// </summary>
    public class PlotGenerator : IPlotGenerator
    {
        #region Construction
        /// <summary>
        /// Construct the PlotGenerator object.
        /// </summary>
        public PlotGenerator()
        {
            PlotAreaLeftPaddingInPixels = int.Parse(Resources.PlotBackground_PlotAreaLeftPadding);
            PlotAreaRightPaddingInPixels = int.Parse(Resources.PlotBackground_PlotAreaRightPadding);
            PlotAreaBottomPaddingInPixels = int.Parse(Resources.PlotBackground_PlotAreaBottomPadding);
            PlotAreaTopPaddingInPixels = int.Parse(Resources.PlotBackground_PlotAreaTopPadding);
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Creates an in-memory image of a plot from the given data.
        /// </summary>
        /// <param name="medianPoint">The median point of the data points.</param>
        /// <param name="dataPoints">The voter data (in world units).</param>
        /// <param name="plotRange">The range of the plot across the image (in world units).</param>
        /// <param name="radiusOfPointCircle">The radius of the circle that will be used to represent a point (in world units).</param>
        /// <param name="alphaOfPointCircle">The alpha value of the circle that will be used to represent a point: [0 - 255].</param>
        /// <returns>The image representation of the plot.</returns>
        public Image CreatePlotImage(Point2D medianPoint, Point2D[] dataPoints, Range2D plotRange, double radiusOfPointCircle, byte alphaOfPointCircle = 16)
        {
            Bitmap bitmap = Resources.PlotBackground;            
            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                // Create world coordinate system.
                FixGraphicsToRange(graphics, plotRange, bitmap.Size);

                // Draw data points.
                using (SolidBrush brush = new SolidBrush(Color.FromArgb(alphaOfPointCircle, Color.Black)))
                {
                    foreach (Point2D dataPoint in dataPoints)
                    {
                        graphics.FillEllipse(
                            brush,
                            (float)(dataPoint.X - radiusOfPointCircle),
                            (float)(dataPoint.Y - radiusOfPointCircle * PlotAreaAspectRatio),
                            (float)(2.0 * radiusOfPointCircle),
                            (float)(2.0 * radiusOfPointCircle * PlotAreaAspectRatio));
                    }
                }

                // Draw the median point.
                using (Pen pen = new Pen(Color.Orange, (float)(0.2 * radiusOfPointCircle)))
                {
                    graphics.DrawLine(
                        pen,
                        (float)(medianPoint.X - radiusOfPointCircle),
                        (float)(medianPoint.Y + radiusOfPointCircle * PlotAreaAspectRatio),
                        (float)(medianPoint.X + radiusOfPointCircle),
                        (float)(medianPoint.Y - radiusOfPointCircle * PlotAreaAspectRatio));

                    graphics.DrawLine(
                        pen,
                        (float)(medianPoint.X - radiusOfPointCircle),
                        (float)(medianPoint.Y - radiusOfPointCircle * PlotAreaAspectRatio),
                        (float)(medianPoint.X + radiusOfPointCircle),
                        (float)(medianPoint.Y + radiusOfPointCircle * PlotAreaAspectRatio));
                }
            }

            return bitmap;
        }
        #endregion

        #region Private Properties
        /// <summary>
        /// Gets or sets the size of the non-plotable area to the left of the plot area.
        /// </summary>
        private int PlotAreaLeftPaddingInPixels { get; set; }

        /// <summary>
        /// Gets or sets the size of the non-plotable area to the right of the plot area.
        /// </summary>
        private int PlotAreaRightPaddingInPixels { get; set; }

        /// <summary>
        /// Gets or sets the size of the non-plotable area below the plot area.
        /// </summary>
        private int PlotAreaBottomPaddingInPixels { get; set; }

        /// <summary>
        /// Gets or sets the size of the non-plotable area above the plot area.
        /// </summary>
        private int PlotAreaTopPaddingInPixels { get; set; }

        /// <summary>
        /// Gets or sets the aspect ratio of the plottable area of the image.
        /// </summary>
        private double PlotAreaAspectRatio { get; set; }
        #endregion

        #region Private Methods
        /// <summary>
        /// Sets up the image's coordinate system so that geometry can be passed in world units.
        /// </summary>
        /// <param name="graphics">The graphics object used to manipulate the image.</param>
        /// <param name="plotRange">The desired range of the plot (in world units).</param>
        /// <param name="sizeOfImageInPixels">The size of the image (in pixels)</param>
        private void FixGraphicsToRange(Graphics graphics, Range2D plotRange, Size sizeOfImageInPixels)
        {
            // Adjust the range to account for the non-plotable parts of the plot background image.
            plotRange.XMin -= (double)PlotAreaLeftPaddingInPixels / (double)sizeOfImageInPixels.Width;
            plotRange.XMax += (double)PlotAreaRightPaddingInPixels / (double)sizeOfImageInPixels.Width;
            plotRange.YMin -= (double)PlotAreaBottomPaddingInPixels / (double)sizeOfImageInPixels.Height;
            plotRange.YMax += (double)PlotAreaTopPaddingInPixels / (double)sizeOfImageInPixels.Height;

            // Caclulate the plot area aspect ratio.
            int plotAreaWidth = sizeOfImageInPixels.Width - (PlotAreaLeftPaddingInPixels + PlotAreaRightPaddingInPixels);
            int plotAreaHeight = sizeOfImageInPixels.Height - (PlotAreaBottomPaddingInPixels + PlotAreaTopPaddingInPixels);
            PlotAreaAspectRatio = (double)plotAreaWidth / (double)plotAreaHeight;

            // The conversion factor from world units to pixel units.
            float worldToPixelScaleX = (float)((double)sizeOfImageInPixels.Width / plotRange.GetWidth());
            float worldToPixelScaleY = (float)((double)sizeOfImageInPixels.Height / plotRange.GetHeight());

            // Flip the y-axis so that the positive y is going up.
            graphics.ScaleTransform(1.0f, -1.0f);
            // Put the origin at the lower-left of the image.
            graphics.TranslateTransform(0.0f, -sizeOfImageInPixels.Height);
            // Scale the world so that it conforms to the plot range.
            graphics.ScaleTransform(worldToPixelScaleX, worldToPixelScaleY);
            // Move the center of the plot range to the center of the image.
            graphics.TranslateTransform((float)plotRange.GetHalfWidth(), (float)plotRange.GetHalfHeight());
        }
        #endregion
    }
}
